/*
 * Copied from Gentyref project http://code.google.com/p/gentyref/
 * Reformatted and moved to fit package structure
 */
package com.vercer.engine.persist.util.generic;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Utility class for doing reflection on types.
 *
 * @author Wouter Coekaerts <wouter@coekaerts.be>
 */
public class GenericTypeReflector
{
	private static final Type UNBOUND_WILDCARD = new WildcardTypeImpl(new Type[] { Object.class },
			new Type[] {});

	/**
	 * Returns the erasure of the given type.
	 */
	public static Class<?> erase(Type type)
	{
		if (type instanceof Class<?>)
		{
			return (Class<?>) type;
		}
		else if (type instanceof ParameterizedType)
		{
			return (Class<?>) ((ParameterizedType) type).getRawType();
		}
		else if (type instanceof TypeVariable<?>)
		{
			TypeVariable<?> tv = (TypeVariable<?>) type;
			if (tv.getBounds().length == 0)
				return Object.class;
			else
				return erase(tv.getBounds()[0]);
		}
		else if (type instanceof GenericArrayType)
		{
			GenericArrayType aType = (GenericArrayType) type;
			return GenericArrayTypeImpl.createArrayType(erase(aType.getGenericComponentType()));
		}
		else
		{
			// TODO at least support CaptureType here
			throw new RuntimeException("not supported: " + type.getClass());
		}
	}

	/**
	 * Maps type parameters in a type to their values.
	 *
	 * @param toMapType
	 *            Type possibly containing type arguments
	 * @param typeAndParams
	 *            must be either ParameterizedType, or (in case there are no
	 *            type arguments, or it's a raw type) Class
	 * @return toMapType, but with type parameters from typeAndParams replaced.
	 */
	private static Type mapTypeParameters(Type toMapType, Type typeAndParams)
	{
		if (isMissingTypeParameters(typeAndParams))
		{
			return erase(toMapType);
		}
		else
		{
			VarMap varMap = new VarMap();
			Type handlingTypeAndParams = typeAndParams;
			while (handlingTypeAndParams instanceof ParameterizedType)
			{
				ParameterizedType pType = (ParameterizedType) handlingTypeAndParams;
				Class<?> clazz = (Class<?>) pType.getRawType(); // getRawType
																// should always
																// be Class
				varMap.addAll(clazz.getTypeParameters(), pType.getActualTypeArguments());
				handlingTypeAndParams = pType.getOwnerType();
			}
			return varMap.map(toMapType);
		}
	}

	/**
	 * Checks if the given type is a class that is supposed to have type
	 * parameters, but doesn't. In other words, if it's a really raw type.
	 */
	private static boolean isMissingTypeParameters(Type type)
	{
		if (type instanceof Class<?>)
		{
			for (Class<?> clazz = (Class<?>) type; clazz != null; clazz = clazz.getEnclosingClass())
			{
				if (clazz.getTypeParameters().length != 0)
					return true;
			}
			return false;
		}
		else if (type instanceof ParameterizedType)
		{
			return false;
		}
		else
		{
			throw new AssertionError("Unexpected type " + type.getClass());
		}
	}

	/**
	 * Returns a type representing the class, with all type parameters the
	 * unbound wildcard ("?"). For example,
	 * <tt>addWildcardParameters(Map.class)</tt> returns a type representing
	 * <tt>Map&lt;?,?&gt;</tt>.
	 *
	 * @return <ul>
	 *         <li>If clazz is a class or interface without type parameters,
	 *         clazz itself is returned.</li>
	 *         <li>If clazz is a class or interface with type parameters, an
	 *         instance of ParameterizedType is returned.</li>
	 *         <li>if clazz is an array type, an array type is returned with
	 *         unbound wildcard parameters added in the the component type.
	 *         </ul>
	 */
	public static Type addWildcardParameters(Class<?> clazz)
	{
		if (clazz.isArray())
		{
			return GenericArrayTypeImpl.createArrayType(addWildcardParameters(clazz
					.getComponentType()));
		}
		else if (isMissingTypeParameters(clazz))
		{
			TypeVariable<?>[] vars = clazz.getTypeParameters();
			Type[] arguments = new Type[vars.length];
			Arrays.fill(arguments, UNBOUND_WILDCARD);
			Type owner = clazz.getDeclaringClass() == null ? null : addWildcardParameters(clazz
					.getDeclaringClass());
			return new ParameterizedTypeImpl(clazz, arguments, owner);
		}
		else
		{
			return clazz;
		}
	}

	/**
	 * With type a supertype of searchClass, returns the exact supertype of the
	 * given class, including type parameters. For example, with
	 * <tt>class StringList implements List&lt;String&gt;</tt>,
	 * <tt>getExactSuperType(StringList.class, Collection.class)</tt> returns a
	 * {@link ParameterizedType} representing <tt>Collection&lt;String&gt;</tt>.
	 * <ul>
	 * <li>Returns null if <tt>searchClass</tt> is not a superclass of type.</li>
	 * <li>Returns an instance of {@link Class} if <tt>type</tt> if it is a raw
	 * type, or has no type parameters</li>
	 * <li>Returns an instance of {@link ParameterizedType} if the type does
	 * have parameters</li>
	 * <li>Returns an instance of {@link GenericArrayType} if
	 * <tt>searchClass</tt> is an array type, and the actual type has type
	 * parameters</li>
	 * </ul>
	 */
	public static Type getExactSuperType(Type type, Class<?> searchClass)
	{
		if (type instanceof ParameterizedType || type instanceof Class<?>
				|| type instanceof GenericArrayType)
		{
			Class<?> clazz = erase(type);

			if (searchClass == clazz)
			{
				return type;
			}

			if (!searchClass.isAssignableFrom(clazz))
				return null;
		}

		for (Type superType : getExactDirectSuperTypes(type))
		{
			Type result = getExactSuperType(superType, searchClass);
			if (result != null)
				return result;
		}

		return null;
	}

	/**
	 * Gets the type parameter for a given type that is the value for a given
	 * type variable. For example, with
	 * <tt>class StringList implements List&lt;String&gt;</tt>,
	 * <tt>getTypeParameter(StringList.class, Collection.class.getTypeParameters()[0])</tt>
	 * returns <tt>String</tt>.
	 *
	 * @param type
	 *            The type to inspect.
	 * @param variable
	 *            The type variable to find the value for.
	 * @return The type parameter for the given variable. Or null if type is not
	 *         a subtype of the type that declares the variable, or if the
	 *         variable isn't known (because of raw types).
	 */
	public static Type getTypeParameter(Type type, TypeVariable<? extends Class<?>> variable)
	{
		Class<?> clazz = variable.getGenericDeclaration();
		Type superType = getExactSuperType(type, clazz);
		if (superType instanceof ParameterizedType)
		{
			int index = Arrays.asList(clazz.getTypeParameters()).indexOf(variable);
			return ((ParameterizedType) superType).getActualTypeArguments()[index];
		}
		else
		{
			return null;
		}
	}

	/**
	 * Checks if the capture of subType is a subtype of superType
	 */
	public static boolean isSuperType(Type superType, Type subType)
	{
		if (superType instanceof ParameterizedType || superType instanceof Class<?>
				|| superType instanceof GenericArrayType)
		{
			Class<?> superClass = erase(superType);
			Type mappedSubType = getExactSuperType(capture(subType), superClass);
			if (mappedSubType == null)
			{
				return false;
			}
			else if (superType instanceof Class<?>)
			{
				return true;
			}
			else if (mappedSubType instanceof Class<?>)
			{
				// TODO treat supertype by being raw type differently
				// ("supertype, but with warnings")
				return true; // class has no parameters, or it's a raw type
			}
			else if (mappedSubType instanceof GenericArrayType)
			{
				Type superComponentType = getArrayComponentType(superType);
				assert superComponentType != null;
				Type mappedSubComponentType = getArrayComponentType(mappedSubType);
				assert mappedSubComponentType != null;
				return isSuperType(superComponentType, mappedSubComponentType);
			}
			else
			{
				assert mappedSubType instanceof ParameterizedType;
				ParameterizedType pMappedSubType = (ParameterizedType) mappedSubType;
				assert pMappedSubType.getRawType() == superClass;
				ParameterizedType pSuperType = (ParameterizedType) superType;

				Type[] superTypeArgs = pSuperType.getActualTypeArguments();
				Type[] subTypeArgs = pMappedSubType.getActualTypeArguments();
				assert superTypeArgs.length == subTypeArgs.length;
				for (int i = 0; i < superTypeArgs.length; i++)
				{
					if (!contains(superTypeArgs[i], subTypeArgs[i]))
					{
						return false;
					}
				}
				// params of the class itself match, so if the owner types are
				// supertypes too, it's a supertype.
				return pSuperType.getOwnerType() == null
						|| isSuperType(pSuperType.getOwnerType(), pMappedSubType.getOwnerType());
			}
		}
		else if (superType instanceof CaptureType)
		{
			if (superType.equals(subType))
				return true;
			for (Type lowerBound : ((CaptureType) superType).getLowerBounds())
			{
				if (isSuperType(lowerBound, subType))
				{
					return true;
				}
			}
			return false;
		}
		else if (superType instanceof GenericArrayType)
		{
			return isArraySupertype(superType, subType);
		}
		else
		{
			throw new RuntimeException("not implemented: " + superType.getClass());
		}
	}

	private static boolean isArraySupertype(Type arraySuperType, Type subType)
	{
		Type superTypeComponent = getArrayComponentType(arraySuperType);
		assert superTypeComponent != null;
		Type subTypeComponent = getArrayComponentType(subType);
		if (subTypeComponent == null)
		{ // subType is not an array type
			return false;
		}
		else
		{
			return isSuperType(superTypeComponent, subTypeComponent);
		}
	}

	/**
	 * If type is an array type, returns the type of the component of the array.
	 * Otherwise, returns null.
	 */
	public static Type getArrayComponentType(Type type)
	{
		if (type instanceof Class<?>)
		{
			Class<?> clazz = (Class<?>) type;
			return clazz.getComponentType();
		}
		else if (type instanceof GenericArrayType)
		{
			GenericArrayType aType = (GenericArrayType) type;
			return aType.getGenericComponentType();
		}
		else
		{
			return null;
		}
	}

	private static boolean contains(Type containingType, Type containedType)
	{
		if (containingType instanceof WildcardType)
		{
			WildcardType wContainingType = (WildcardType) containingType;
			for (Type upperBound : wContainingType.getUpperBounds())
			{
				if (!isSuperType(upperBound, containedType))
				{
					return false;
				}
			}
			for (Type lowerBound : wContainingType.getLowerBounds())
			{
				if (!isSuperType(containedType, lowerBound))
				{
					return false;
				}
			}
			return true;
		}
		else
		{
			return containingType.equals(containedType);
		}
	}

	/**
	 * Returns the direct supertypes of the given type. Resolves type
	 * parameters.
	 */
	public static Type[] getExactDirectSuperTypes(Type type)
	{
		if (type instanceof ParameterizedType || type instanceof Class<?>)
		{
			Class<?> clazz;
			if (type instanceof ParameterizedType)
			{
				clazz = (Class<?>) ((ParameterizedType) type).getRawType();
			}
			else
			{
				// TODO primitive types?
				clazz = (Class<?>) type;
				if (clazz.isArray())
					return getArrayExactDirectSuperTypes(clazz);
			}

			Type[] superInterfaces = clazz.getGenericInterfaces();
			Type superClass = clazz.getGenericSuperclass();
			Type[] result;
			int resultIndex;
			if (superClass == null)
			{
				result = new Type[superInterfaces.length];
				resultIndex = 0;
			}
			else
			{
				result = new Type[superInterfaces.length + 1];
				resultIndex = 1;
				result[0] = mapTypeParameters(superClass, type);
			}
			for (Type superInterface : superInterfaces)
			{
				result[resultIndex++] = mapTypeParameters(superInterface, type);
			}

			return result;
		}
		else if (type instanceof TypeVariable<?>)
		{
			TypeVariable<?> tv = (TypeVariable<?>) type;
			return tv.getBounds();
		}
		else if (type instanceof WildcardType)
		{
			// This should be a rare case: normally this wildcard is already
			// captured.
			// But it does happen if the upper bound of a type variable contains
			// a wildcard
			// TODO shouldn't upper bound of type variable have been captured
			// too? (making this case impossible?)
			return ((WildcardType) type).getUpperBounds();
		}
		else if (type instanceof CaptureType)
		{
			return ((CaptureType) type).getUpperBounds();
		}
		else if (type instanceof GenericArrayType)
		{
			return getArrayExactDirectSuperTypes(type);
		}
		else
		{
			throw new RuntimeException("not implemented type: " + type);
		}
	}

	private static Type[] getArrayExactDirectSuperTypes(Type arrayType)
	{
		// see
		// http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.10.3
		Type typeComponent = getArrayComponentType(arrayType);

		Type[] result;
		int resultIndex;
		if (typeComponent instanceof Class<?> && ((Class<?>) typeComponent).isPrimitive())
		{
			resultIndex = 0;
			result = new Type[3];
		}
		else
		{
			Type[] componentSupertypes = getExactDirectSuperTypes(typeComponent);
			result = new Type[componentSupertypes.length + 3];
			for (resultIndex = 0; resultIndex < componentSupertypes.length; resultIndex++)
			{
				result[resultIndex] = GenericArrayTypeImpl
						.createArrayType(componentSupertypes[resultIndex]);
			}
		}
		result[resultIndex++] = Object.class;
		result[resultIndex++] = Cloneable.class;
		result[resultIndex++] = Serializable.class;
		return result;
	}

	/**
	 * Returns the exact return type of the given method in the given type. This
	 * may be different from <tt>m.getGenericReturnType()</tt> when the method
	 * was declared in a superclass, of <tt>type</tt> is a raw type.
	 */
	public static Type getExactReturnType(Method m, Type type)
	{
		Type returnType = m.getGenericReturnType();
		Type exactDeclaringType = getExactSuperType(capture(type), m.getDeclaringClass());
		return mapTypeParameters(returnType, exactDeclaringType);
	}

	/**
	 * Returns the exact type of the given field in the given type. This may be
	 * different from <tt>f.getGenericType()</tt> when the field was declared in
	 * a superclass, of <tt>type</tt> is a raw type.
	 */
	public static Type getExactFieldType(Field f, Type type)
	{
		Type returnType = f.getGenericType();
		Type exactDeclaringType = getExactSuperType(capture(type), f.getDeclaringClass());
		return mapTypeParameters(returnType, exactDeclaringType);
	}

	/**
	 * Applies capture conversion to the given type.
	 */
	public static Type capture(Type type)
	{
		VarMap varMap = new VarMap();
		List<CaptureTypeImpl> toInit = new ArrayList<CaptureTypeImpl>();
		if (type instanceof ParameterizedType)
		{
			ParameterizedType pType = (ParameterizedType) type;
			Class<?> clazz = (Class<?>) pType.getRawType();
			Type[] arguments = pType.getActualTypeArguments();
			TypeVariable<?>[] vars = clazz.getTypeParameters();
			Type[] capturedArguments = new Type[arguments.length];
			assert arguments.length == vars.length;
			for (int i = 0; i < arguments.length; i++)
			{
				Type argument = arguments[i];
				if (argument instanceof WildcardType)
				{
					CaptureTypeImpl captured = new CaptureTypeImpl((WildcardType) argument, vars[i]);
					argument = captured;
					toInit.add(captured);
				}
				capturedArguments[i] = argument;
				varMap.add(vars[i], argument);
			}
			for (CaptureTypeImpl captured : toInit)
			{
				captured.init(varMap);
			}
			Type ownerType = (pType.getOwnerType() == null) ? null : capture(pType.getOwnerType());
			return new ParameterizedTypeImpl(clazz, capturedArguments, ownerType);
		}
		else
		{
			return type;
		}
	}

	/**
	 * Returns the display name of a Type.
	 */
	public static String getTypeName(Type type)
	{
		if (type instanceof Class<?>)
		{
			Class<?> clazz = (Class<?>) type;
			return clazz.isArray() ? (getTypeName(clazz.getComponentType()) + "[]") : clazz.getName();
		}
		else
		{
			return type.toString();
		}
	}
}
